import Scene from './scene'
import Alg from '../algorithm/index'
import Box2D from '../libs/box2d'
import gameInfo from '../runtime/astray'
import explodeAnimation from '../base/explodeAnimation'
import util from '../base/util'

const screenWidth = window.innerWidth
const screenHeight = window.innerHeight

// Box2D shortcuts
let b2World        = Box2D.Dynamics.b2World
let b2FixtureDef   = Box2D.Dynamics.b2FixtureDef
let b2BodyDef      = Box2D.Dynamics.b2BodyDef
let b2Body		   = Box2D.Dynamics.b2Body
let b2CircleShape  = Box2D.Collision.Shapes.b2CircleShape
let b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape
let b2Settings     = Box2D.Common.b2Settings
let b2Vec2         = Box2D.Common.Math.b2Vec2

class system{
  constructor(parent, name){
    this.parent = parent
    this.name = name
  }
  initializePhysicsWorld(){
  }
  updatePhysicsWorld(){

  }
  async initializeRenderWorld(){
  }
  updateRenderWorld(){
  }
  async handleItemEvent(type){
  }
}

class bombSystem extends system{
  constructor(parent){
    super(parent, "bomb")
    this.explosions = []
  }
  __createPhysicsObject(pos){
    let bodyDef = new b2BodyDef()
    let fixDef = new b2FixtureDef()
    let npc = this.parent.__createPhysicsBall(bodyDef, fixDef, pos, 0.2, 10, 10, 1000)
    npc.radius = 0.2
    npc.type = this.name
    this.parent.npcs.push(npc)
    return npc
  }
  initializePhysicsWorld(){
    for (let i = 0; i < this.parent.lv; ++i){
      this.__createPhysicsObject(this.parent.__generateRandomPosition())
    }
  }
  updatePhysicsWorld(){
    for (let i = this.parent.npcs.length - 1; i >= 0; --i){
      let npc = this.parent.npcs[i]
      if (npc.type == this.name){
        let v = npc.GetLinearVelocity()
        if (v.x || v.y){
          if (npc.status == 1){
            let pos = npc.GetPosition()
            npc.status = {x: pos.x, y: pos.y}
            this.parent.wWorld.DestroyBody(npc)
          }
        }else
          npc.status = 1
      }
    }
  }
  async initializeRenderWorld(){
    for (let i in this.parent.npcs){
      let npc = this.parent.npcs[i]
      if (npc.type == this.name)
        npc.fig = await this.parent.__createRenderBall({map: 'images/astray/' + this.name + '.png'}, npc.pos.x, npc.pos.y, npc.radius)
    }
  }
  updateRenderWorld(){
    for (let i = this.parent.npcs.length - 1; i >= 0; --i){
      let npc = this.parent.npcs[i]
      if (npc.type == this.name && npc.status && npc.status != 1){
          this.explosions.push(new explodeAnimation(npc.status.x, npc.status.y, this.parent.THREE, this.parent.scene))
          this.parent.scene.remove(npc.fig)
          npc.type = ""
      }
    }
    let pCount = this.explosions.length
    while(pCount--) {
      this.explosions[pCount].update();
    }
  }
  async handleItemEvent(type){
    if (type == this.name){
      let npc = this.__createPhysicsObject(this.parent.wBall.GetPosition())
      npc.type = this.name
      npc.fig = await this.parent.__createRenderBall({map: 'images/astray/' + this.name + '.png'}, npc.pos.x, npc.pos.y, npc.radius)
      return true
    }
    return false
  }
}

class barriarSystem extends system{
  constructor(parent){
    super(parent, "barriar")
  }
  async initializePhysicsWorld(){
    let bodyDef = new b2BodyDef()
    let fixDef = new b2FixtureDef()
    for (let i = 0; i < this.parent.lv; ++i){
      let npc = this.parent.__createPhysicsBall(bodyDef, fixDef, this.parent.__generateRandomPosition(), 0.28, 4)
      npc.radius = 0.28
      npc.type = this.name
      this.parent.npcs.push(npc)
    }
  }
  async initializeRenderWorld(){
    for (let i in this.parent.npcs){
      let npc = this.parent.npcs[i]
      if (npc.type == this.name){
        npc.fig = await this.parent.__createRenderBall({color: "#00ffff"}, npc.pos.x, npc.pos.y, npc.radius)
      }
    }
  }

  updateRenderWorld(){
    for (let i = this.parent.npcs.length - 1; i >= 0; --i){
      let npc = this.parent.npcs[i]
      if (npc.type == this.name)
        this.parent.__rollBall(npc, npc.fig, npc.radius)
    }
  }
}

class shieldSystem extends system{
  constructor(parent){
    super(parent, "shield")
    this.remain = 0

    this.vertex_shader = `    
      precision lowp float;
      precision lowp int;
      uniform mat4 modelViewMatrix;
      uniform mat4 projectionMatrix;
      attribute vec3 position;
      varying vec3 vPosition;
      void main() {
          vPosition = position;
          gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
      }
    `
    this.fragment_shader = `
      precision lowp float;
      precision lowp int;
      uniform float time;
      varying vec3 vPosition;
      void main(void){
        vec3 position = - 1.0 + 2.0 * vPosition;
        float red = abs(sin(position.x * position.y + time / 1.0));
        float green = abs(sin(position.x * position.y + time / 3.0));
        float blue = abs(sin(position.x * position.y + time / 6.0));
        gl_FragColor = vec4(red, green, blue, 1.0);
      }
    `
  }
  updateBallStatus(density, material){
    this.parent.wBall.GetFixtureList().SetDensity(density)
    this.parent.wBall.ResetMassData()
    this.parent.ballMesh.material = material
    this.parent.ballMesh.material.needsUpdate = true
  }
  updateState(isfirst){
    if (isfirst){
      this.updateBallStatus(100, new this.parent.THREE.RawShaderMaterial({ 
        uniforms: {
            time: {
                value: this.remain
            }
        },
        vertexShader: this.vertex_shader,
        fragmentShader: this.fragment_shader, 
      }))
      this.parent.ballMesh.material.uniforms.time.value = this.remain
    }
    setTimeout(e => {
      this.remain--
      if (this.remain)
        this.updateState()
      else
        this.updateBallStatus(0.2, new this.parent.THREE.MeshPhongMaterial({color: "#ff0000"}))
    }, 1000)
  }
  async handleItemEvent(type){
    if (type == this.name){
        this.remain += 10
        if (this.remain == 10)
          this.updateState(true)
    }
  }
}

class smallSystem extends system{
  constructor(parent){
    super(parent, "small")
    this.remain = 0
  }
  updateBallStatus(radius){
    let fx = this.parent.wBall.GetFixtureList()
    let fixDef = new b2FixtureDef()
    fixDef.density = fx.GetDensity()
    fixDef.friction = fx.GetFriction()
    fixDef.restitution = fx.GetRestitution()
    fixDef.shape = new b2CircleShape(radius)
    this.parent.wBall.DestroyFixture(fx)
    this.parent.wBall.CreateFixture(fixDef)
    this.parent.ballMesh.scale.set(radius / 0.2, radius / 0.2, radius / 0.2)
  }
  updateState(isfirst){
    if (isfirst){
      this.updateBallStatus(0.1)
    }
    setTimeout(e => {
      this.remain--
      if (this.remain)
        this.updateState()
      else
        this.updateBallStatus(0.2)
    }, 1000)
  }
  async handleItemEvent(type){
    if (type == this.name){
        this.remain += 10
        if (this.remain == 10)
          this.updateState(true)
    }
  }
}

export default class astrayScene extends Scene {
  constructor(aParent, aOpenID) {
    super(aParent, aOpenID)
    this.ballRadius = 0.2
    this.mazeDimension = 11
    this.keyAxis = [0, 0]
    
    this.info = new gameInfo()

    this.bindTouchStart = this.__touchStart.bind(this)
    this.bindTouchMove = this.__touchMove.bind(this)
    this.bindTouchEnd = this.__touchEnd.bind(this)
    this.systems = [new bombSystem(this), new barriarSystem(this), new shieldSystem(this), new smallSystem(this)]

    this.worldStep = 1 / 180
  }

  __createPhysicsBall(bodyDef, fixDef, pos, radius, density = 1.0, restitution = 0.25, friction = 0.0){
    bodyDef.type = b2Body.b2_dynamicBody
    bodyDef.position.Set(pos.x, pos.y)
    let ret = this.wWorld.CreateBody(bodyDef)
    ret.pos = pos
    fixDef.density = density
    fixDef.friction = friction
    fixDef.restitution = restitution
    fixDef.shape = new b2CircleShape(radius)
    ret.CreateFixture(fixDef)
    return ret
  }

  __impulse(ball, direction){
    // Apply "friction". 
    var lv = ball.GetLinearVelocity()
    lv.Multiply(0.95)
    ball.SetLinearVelocity(lv)
    
    // Apply user-directed force.
    var f = new b2Vec2(direction[0] * ball.GetMass() * 0.25, direction[1] * ball.GetMass() * 0.25)
    ball.ApplyImpulse(f, ball.GetPosition())   
  }

  __initializePhysicsWorld(){
    // Create the world object.
    this.wWorld = new b2World(new b2Vec2(0, 0), true)
    let bodyDef = new b2BodyDef()
    let fixDef = new b2FixtureDef()
    // Create the ball.
    this.wBall = this.__createPhysicsBall(bodyDef, fixDef, {x: 1, y: 1}, this.ballRadius, 0.2)

    // Create the maze.
    bodyDef.type = b2Body.b2_staticBody;
    fixDef.shape = new b2PolygonShape();
    fixDef.shape.SetAsBox(0.5, 0.5);
    for (var i = 0; i < this.maze.dimension; i++) {
        for (var j = 0; j < this.maze.dimension; j++) {
            if (this.maze[i][j]) {
                bodyDef.position.x = i;
                bodyDef.position.y = j;
                this.wWorld.CreateBody(bodyDef).CreateFixture(fixDef);
            }
        }
    }

    // Create npcs
    this.npcs = []
    for (let i in this.systems)
      this.systems[i].initializePhysicsWorld()
  }

  __generate_maze_mesh(field, texture) {
    var dummy = new this.THREE.Geometry();
    for (var i = 0; i < field.dimension; i++) {
        for (var j = 0; j < field.dimension; j++) {
            if (field[i][j]) {
                var geometry = new this.THREE.CubeGeometry(1,1,1,1,1,1);
                var mesh_ij = new this.THREE.Mesh(geometry);
                mesh_ij.position.x = i;
                mesh_ij.position.y = j;
                mesh_ij.position.z = 0.5;
                mesh_ij.updateMatrix()
                dummy.merge(mesh_ij.geometry, mesh_ij.matrix)
            }
        }
    }
    var material = new this.THREE.MeshPhongMaterial({map: texture});
    var mesh = new this.THREE.Mesh(dummy, material)
    return mesh;
}

  async __createRenderBall(txt, x, y, radius){
    let g = new this.THREE.SphereGeometry(radius, 32, 16)
    if (txt.map){
      txt.map = await this.loader.load(txt.map)
    }
    let m = new this.THREE.MeshPhongMaterial(txt)
    let ret = new this.THREE.Mesh(g, m)
    ret.position.set(x, y, radius)
    ret.rotation.y = - Math.PI / 2
    //ret.rotation.set(new this.THREE.Vector3( 0,0, 0))
    this.scene.add(ret)
    return ret
  }

  __generateRandomPosition(){
    do{
      let x = Math.floor(Math.random() * this.mazeDimension)
      let y = Math.floor(Math.random() * this.mazeDimension)
      if (!this.maze[x][y] && (x != 1 && y != 1)){
        if (!this.prop_sets[x][y])
          return {x: x, y: y}
      }
    }while(1)
  }

  //http://cache.baiducontent.com/c?m=ITgwwthMLb_isPURjq2FknheZgIfqsz5JkveH1idnvL1jx7IGBJ9dxnF97q6T-wjivnVooOnFW8YSvba3_g1PBJXaljpmhLSGGGz7tOMpdkVKwCBhy_qUl0tl1jYfkKzekp0s_phAgl64s8GQrzhjdYxt-KMKpRHzyYHcoVU9PAoozaqtKwbakjqHMn4Mdid&p=9b6ec415d9c441b90be296364e05&newp=8b2a970a8d9502f843bd9b790d6492695d0fc20e3bdcd201298ffe0cc4241a1a1a3aecbe2524160ed3c5786d07af4e58e1f434743d0034f1f689df08d2ecce7e60d371772707&s=8097c99f0e839a57&user=baidu&fm=sc&query=threejs+%BC%D3%D4%D8obj%C4%A3%D0%CD&qid=c6674a2600073e6c&p1=1
  async __create3DModel(obj_path, mtl_path){
    let mtl_loader = new this.THREE.MTLLoader()
    let mtl = await mtl_loader.load(mtl_path)
    mtl.preload()
    let obj_loader = new this.THREE.OBJLoader()
    obj_loader.setMaterials(mtl)
    let obj = await obj_loader.load(obj_path)
    return obj
  }

  async __initializeRenderWorld(){
    // Add the light.
    this.light= new this.THREE.PointLight(0xffffff, 1)
    this.light.position.set(1, 1, 1.3)
    this.scene.add(this.light)
    
    // Add the ball.
    this.ballMesh = await this.__createRenderBall({color: "#ff0000"}, 1, 1, this.ballRadius)
    //this.ballMesh = await this.__create3DModel('images/astray/3d/spider.obj', 'images/astray/3d/spider.mtl')
    for (let i in this.systems)
      await this.systems[i].initializeRenderWorld()
    //initialize maze
    this.loader.load('images/astray/brick.png', t => {
      this.scene.add(this.__generate_maze_mesh(this.maze, t))
    })
    
    // Add the ground.
    this.loader.load('images/astray/concrete.png', t => {
      let g = new this.THREE.PlaneGeometry(this.mazeDimension * 10, this.mazeDimension * 10, this.mazeDimension, this.mazeDimension)
      t.wrapS = t.wrapT = this.THREE.RepeatWrapping
      t.repeat.set(this.mazeDimension * 5, this.mazeDimension * 5)
      let m = new this.THREE.MeshPhongMaterial({map:t})
      let planeMesh = new this.THREE.Mesh(g, m)
      //planeMesh.position.set((this.mazeDimension - 1) / 2, (this.mazeDimension - 1) / 2, 0);
      //planeMesh.rotation.set(Math.PI/2, 0, 0);
      this.scene.add(planeMesh)
    })

    // Add properties 
    this.loader.load('images/astray/coin.png', t => {
      for (let i = 0; i < this.lv; ++i){
        let c = new this.THREE.CylinderGeometry(0.25, 0.25, 0.1, 20, 1)
        let m = new this.THREE.MeshPhongMaterial({map: t})
        let t2 = t.clone()
        t2.needsUpdate = true
        t2.flipY = false
        let m2 = new this.THREE.MeshPhongMaterial({map: t2})
        let m0 = new this.THREE.MeshPhongMaterial({color: 0xffcc33})
        let coinMesh = new this.THREE.Mesh(c, [m0, m, m2])
        let pos = this.__generateRandomPosition()
        coinMesh.proptype = "coin"
        this.prop_sets[pos.x][pos.y] = coinMesh
        coinMesh.position.set(pos.x, pos.y, 0.25)
        coinMesh.rotation.set(Math.PI / 2, Math.PI / 2, 0)
        this.scene.add(coinMesh)
      }
    })
  }

  __updatePhysicsWorld() {
    this.__impulse(this.wBall, this.keyAxis)
    for (let i in this.systems)
      this.systems[i].updatePhysicsWorld()
    // Take a time step.
    this.wWorld.Step(this.worldStep, 8, 3)
}

__rotateMatrix(){
  let mx = arguments[0]
  for (let i = 1; i < arguments.length; i += 2)
    mx = new this.THREE.Matrix4().makeRotationAxis(arguments[i], arguments[i + 1]).multiply(mx)
  return mx
}


__rollBall(physicsBall, renderBall, radius){
  // Update ball position.
  var stepX = physicsBall.GetPosition().x - renderBall.position.x;
  var stepY = physicsBall.GetPosition().y - renderBall.position.y;

  renderBall.position.x += stepX;
  renderBall.position.y += stepY;

  // Update ball rotation.
  renderBall.matrix = this.__rotateMatrix(renderBall.matrix, 
    new this.THREE.Vector3(0,1,0), stepX / radius, 
    new this.THREE.Vector3(1,0,0), -stepY / radius)
  renderBall.rotation.setFromRotationMatrix(renderBall.matrix)
}

__updateRenderWorld() {
  let player = this.ballMesh
  this.__rollBall(this.wBall, player, this.ballRadius)
  for (let i in this.systems)
    this.systems[i].updateRenderWorld()

  // Update camera and light positions.
  let dx = (player.position.x - this.camera.position.x) * 0.1,
    dy = (player.position.y - this.camera.position.y) * 0.1
  this.camera.position.x += dx
  this.camera.position.y += dy
  this.camera.position.z += (5 - this.camera.position.z) * 0.1;
  this.light.position.x = this.camera.position.x;
  this.light.position.y = this.camera.position.y;
  this.light.position.z = this.camera.position.z - 3.7;
  this.info.updateScene(dx, dy)

  // Update properties 
  let x = Math.floor(player.position.x), y = Math.floor(player.position.y)
  let sx = Math.max(0, x - 2), ex = Math.min(this.mazeDimension - 1, x + 2),
    sy = Math.max(0, y - 3), ey = Math.min(this.mazeDimension - 1, y + 3)
  for (let i = sx; i <= ex; ++i)
    for (let j = sy; j <= ey; ++j){
      let prop = this.prop_sets[i][j]
      if (prop && prop.proptype == "coin"){
        prop.matrix = this.__rotateMatrix(prop.matrix, new this.THREE.Vector3(0,1,0), 0.05)
        prop.rotation.setFromRotationMatrix(prop.matrix)
      }
    }
  
  // Interact properties
  x = Math.round(player.position.x)
  y = Math.round(player.position.y)
  if (x < this.mazeDimension && y < this.mazeDimension && this.prop_sets[x][y] && this.prop_sets[x][y].proptype == "coin"){
    this.scene.remove(this.prop_sets[x][y])
    this.prop_sets[x][y] = null
    this.info.updateCoins()
  }
}

  async __gameLoop(){
    switch(this.gameState) {      
      case 'initialize':
          this.maze = Alg.generateSquareMaze(this.mazeDimension)

          this.prop_sets = new Array(this.mazeDimension)
          for(var i = 0; i < this.mazeDimension; i++) {
            this.prop_sets[i] = new Array(this.mazeDimension)
            for (var j = 0; j < this.mazeDimension; j++) {
              this.prop_sets[i][j] = false
            }
          }

          this.maze[this.mazeDimension - 1][this.mazeDimension - 2] = false
          this.lv = Math.floor((this.mazeDimension-1)/2 - 4)
          this.__initializePhysicsWorld()
          await this.__initializeRenderWorld()
          this.camera.position.set(1, 1, 5)
          this.light.position.set(1, 1, 1.3)
          this.light.intensity = 0
          this.info.initialize(this.scene, this.THREE, this.lv, this.openid, this.loader, this.camera)
          this.gameState = 'fade in'
          break;
      case 'fade in':
          this.light.intensity += 0.1 * (1.0 - this.light.intensity);
          if (Math.abs(this.light.intensity - 1.0) < 0.05) {
              this.light.intensity = 1.0;
              this.gameState = 'play'
          }
          break
      case 'play':
          this.__updatePhysicsWorld()
          this.__updateRenderWorld()

          // Check for victory.
          var mazeX = Math.floor(this.ballMesh.position.x + 0.5);
          var mazeY = Math.floor(this.ballMesh.position.y + 0.5);
          if (mazeX == this.mazeDimension && mazeY == this.mazeDimension - 2) { 
              this.gameState = 'fade out';
          }else
            this.info.updateTime()
          break
  
      case 'fade out':
          //this.__updatePhysicsWorld()
          //this.__updateRenderWorld()
          this.light.intensity += 0.1 * (0.0 - this.light.intensity)
          if (Math.abs(this.light.intensity - 0.0) < 0.1) {
              this.info.finish("single")
              this.light.intensity = 0.0
              this.first_render = true
              this.gameState = 'initialize'
              this.mazeDimension += 2
          }
          break    
    }
  }

  __tryMove(x, y){
    let ct = {x: canvas.width / 2, y: canvas.height / 2}
    this.keyAxis = [x > ct.x ? 1 : x < ct.x ? - 1 : 0, y > ct.y ? - 1 : y < ct.y ? 1: 0]
  }

  async __touchStart(e){
    e.preventDefault()
    const x = e.touches[0].clientX
    const y = e.touches[0].clientY
    
    
    let ret = this.info.control(e.touches[0].clientX, e.touches[0].clientY)
    if (ret == "exit")
      this.toNextScene("loading")
    else{
      for (let i in this.systems)
        if (await this.systems[i].handleItemEvent(ret))
          return
      this.__tryMove(x, y)
    }
  }

  __touchEnd(e){
    e.preventDefault()
    this.keyAxis = [0, 0]
  }

  __touchMove(e){
    e.preventDefault()
    const x = e.touches[0].clientX
    const y = e.touches[0].clientY
    this.__tryMove(x, y)
  }

  update(){
    return Scene.prototype.update.apply(this, arguments)
    //combined to gameloop
  }
  async render(ctx){
    ctx.update(async (THREE, scene, loader, camera) => {
      if (this.first_render){
        Scene.prototype.render.apply(this, arguments)
        this.THREE = THREE
        this.scene = scene
        this.loader = loader
        this.camera = camera
        this.gameState = 'initialize'
      }
      await this.__gameLoop()
    })
  }

  enter(aInfo){
    /*this.first_render = false
    this.gameState = "???"
    util.request({
      api: "astray/listrecords",
      data: {player: this.openid, 
             type: 'single'}})
    .then(res => {
        let rec = res.data
        rec.sort((p, n) => {
          return n.stage - p.stage
        })
        if (rec.length)
          this.mazeDimension = (rec[0].stage + 5) * 2 + 1
        else
          this.mazeDimension = 11
    })*/
    this.match_id = aInfo.match_id
    this.mazeDimension = (aInfo.stage + 4) * 2 + 1
    Scene.prototype.enter.apply(this, arguments)
    canvas.addEventListener('touchstart', this.bindTouchStart)
    canvas.addEventListener('touchmove', this.bindTouchMove)
    canvas.addEventListener('touchend', this.bindTouchEnd)
  }

  leave(){
    canvas.removeEventListener('touchstart', this.bindTouchStart)
    canvas.removeEventListener('touchmove', this.bindTouchMove)
    canvas.removeEventListener('touchend', this.bindTouchEnd)
  }
}